/*
 * Copyright (C) 2015 Baidu, Inc. All Rights Reserved.
 */

package com.baidu.mapapi.clusterutil.clustering;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;

import com.baidu.mapapi.clusterutil.MarkerManager;
import com.baidu.mapapi.clusterutil.clustering.algo.Algorithm;
import com.baidu.mapapi.clusterutil.clustering.algo.NonHierarchicalDistanceBasedAlgorithm;
import com.baidu.mapapi.clusterutil.clustering.algo.PreCachingAlgorithmDecorator;
import com.baidu.mapapi.clusterutil.clustering.view.ClusterRenderer;
import com.baidu.mapapi.clusterutil.clustering.view.DefaultClusterRenderer;
import com.baidu.mapapi.map.BaiduMap;
import com.baidu.mapapi.map.MapStatus;
import com.baidu.mapapi.map.Marker;

/**
 * Groups many items on a map based on zoom level.
 * <p/>
 * ClusterManager should be added to the map
 * <li>
 */
public class ClusterManager<T extends ClusterItem> implements
		BaiduMap.OnMapStatusChangeListener, BaiduMap.OnMarkerClickListener {
	private final MarkerManager mMarkerManager;
	private final MarkerManager.Collection mMarkers;
	private final MarkerManager.Collection mClusterMarkers;

	private Algorithm<T> mAlgorithm;
	private final ReadWriteLock mAlgorithmLock = new ReentrantReadWriteLock();
	private ClusterRenderer<T> mRenderer;

	private BaiduMap mMap;
	private MapStatus mPreviousCameraPosition;
	private ClusterTask mClusterTask;
	private final ReadWriteLock mClusterTaskLock = new ReentrantReadWriteLock();

	private OnClusterItemClickListener<T> mOnClusterItemClickListener;
	private OnClusterInfoWindowClickListener<T> mOnClusterInfoWindowClickListener;
	private OnClusterItemInfoWindowClickListener<T> mOnClusterItemInfoWindowClickListener;
	private OnClusterClickListener<T> mOnClusterClickListener;

	public ClusterManager(Context context, BaiduMap map) {
		this(context, map, new MarkerManager(map));
	}

	public ClusterManager(Context context, BaiduMap map,
			MarkerManager markerManager) {
		mMap = map;
		mMarkerManager = markerManager;
		mClusterMarkers = markerManager.newCollection();
		mMarkers = markerManager.newCollection();
		mRenderer = new DefaultClusterRenderer<T>(context, map, this);
		mAlgorithm = new PreCachingAlgorithmDecorator<T>(
				new NonHierarchicalDistanceBasedAlgorithm<T>());
		mClusterTask = new ClusterTask();
		mRenderer.onAdd();
	}

	public MarkerManager.Collection getMarkerCollection() {
		return mMarkers;
	}

	public MarkerManager.Collection getClusterMarkerCollection() {
		return mClusterMarkers;
	}

	public MarkerManager getMarkerManager() {
		return mMarkerManager;
	}

	public void setRenderer(ClusterRenderer<T> view) {
		mRenderer.setOnClusterClickListener(null);
		mRenderer.setOnClusterItemClickListener(null);
		mClusterMarkers.clear();
		mMarkers.clear();
		mRenderer.onRemove();
		mRenderer = view;
		mRenderer.onAdd();
		mRenderer.setOnClusterClickListener(mOnClusterClickListener);
		mRenderer
				.setOnClusterInfoWindowClickListener(mOnClusterInfoWindowClickListener);
		mRenderer.setOnClusterItemClickListener(mOnClusterItemClickListener);
		mRenderer
				.setOnClusterItemInfoWindowClickListener(mOnClusterItemInfoWindowClickListener);
		cluster();
	}

	public void setAlgorithm(Algorithm<T> algorithm) {
		mAlgorithmLock.writeLock().lock();
		try {
			if (mAlgorithm != null) {
				algorithm.addItems(mAlgorithm.getItems());
			}
			mAlgorithm = new PreCachingAlgorithmDecorator<T>(algorithm);
		} finally {
			mAlgorithmLock.writeLock().unlock();
		}
		cluster();
	}

	public void clearItems() {
		mAlgorithmLock.writeLock().lock();
		try {
			mAlgorithm.clearItems();
		} finally {
			mAlgorithmLock.writeLock().unlock();
		}
	}

	public void addItems(Collection<T> items) {
		mAlgorithmLock.writeLock().lock();
		try {
			mAlgorithm.addItems(items);
		} finally {
			mAlgorithmLock.writeLock().unlock();
		}

	}

	public void addItem(T myItem) {
		mAlgorithmLock.writeLock().lock();
		try {
			mAlgorithm.addItem(myItem);
		} finally {
			mAlgorithmLock.writeLock().unlock();
		}
	}

	public void removeItem(T item) {
		mAlgorithmLock.writeLock().lock();
		try {
			mAlgorithm.removeItem(item);
		} finally {
			mAlgorithmLock.writeLock().unlock();
		}
	}

	/**
	 * Force a re-cluster. You may want to call this after adding new item(s).
	 */
	public void cluster() {
		mClusterTaskLock.writeLock().lock();
		try {
			// Attempt to cancel the in-flight request.
			mClusterTask.cancel(true);
			mClusterTask = new ClusterTask();

			if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
				mClusterTask.execute(mMap.getMapStatus().zoom);
			} else {
				mClusterTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
						mMap.getMapStatus().zoom);
			}
		} finally {
			mClusterTaskLock.writeLock().unlock();
		}
	}

	@Override
	public void onMapStatusChangeStart(MapStatus mapStatus) {

	}

	@Override
	public void onMapStatusChange(MapStatus mapStatus) {
		if (mRenderer instanceof BaiduMap.OnMapStatusChangeListener) {
			((BaiduMap.OnMapStatusChangeListener) mRenderer)
					.onMapStatusChange(mapStatus);
		}

		// Don't re-compute clusters if the map has just been
		// panned/tilted/rotated.
		MapStatus position = mMap.getMapStatus();
		if (mPreviousCameraPosition != null
				&& mPreviousCameraPosition.zoom == position.zoom) {
			return;
		}
		mPreviousCameraPosition = mMap.getMapStatus();

		cluster();
	}

	@Override
	public void onMapStatusChangeFinish(MapStatus mapStatus) {

	}

	@Override
	public boolean onMarkerClick(Marker marker) {
		return false;
	}

	/**
	 * Runs the clustering algorithm in a background thread, then re-paints when
	 * results come back.
	 */
	private class ClusterTask extends
			AsyncTask<Float, Void, Set<? extends Cluster<T>>> {
		@Override
		protected Set<? extends Cluster<T>> doInBackground(Float... zoom) {
			mAlgorithmLock.readLock().lock();
			try {
				return mAlgorithm.getClusters(zoom[0]);
			} finally {
				mAlgorithmLock.readLock().unlock();
			}
		}

		@Override
		protected void onPostExecute(Set<? extends Cluster<T>> clusters) {
			mRenderer.onClustersChanged(clusters);
		}
	}

	/**
	 * Sets a callback that's invoked when a Cluster is tapped. Note: For this
	 * listener to function, the ClusterManager must be added as a click
	 * listener to the map.
	 */
	public void setOnClusterClickListener(OnClusterClickListener<T> listener) {
		mOnClusterClickListener = listener;
		mRenderer.setOnClusterClickListener(listener);
	}

	/**
	 * Sets a callback that's invoked when a Cluster is tapped. Note: For this
	 * listener to function, the ClusterManager must be added as a info window
	 * click listener to the map.
	 */
	public void setOnClusterInfoWindowClickListener(
			OnClusterInfoWindowClickListener<T> listener) {
		mOnClusterInfoWindowClickListener = listener;
		mRenderer.setOnClusterInfoWindowClickListener(listener);
	}

	/**
	 * Sets a callback that's invoked when an individual ClusterItem is tapped.
	 * Note: For this listener to function, the ClusterManager must be added as
	 * a click listener to the map.
	 */
	public void setOnClusterItemClickListener(
			OnClusterItemClickListener<T> listener) {
		mOnClusterItemClickListener = listener;
		mRenderer.setOnClusterItemClickListener(listener);
	}

	/**
	 * Sets a callback that's invoked when an individual ClusterItem's Info
	 * Window is tapped. Note: For this listener to function, the ClusterManager
	 * must be added as a info window click listener to the map.
	 */
	public void setOnClusterItemInfoWindowClickListener(
			OnClusterItemInfoWindowClickListener<T> listener) {
		mOnClusterItemInfoWindowClickListener = listener;
		mRenderer.setOnClusterItemInfoWindowClickListener(listener);
	}

	/**
	 * Called when a Cluster is clicked.
	 */
	public interface OnClusterClickListener<T extends ClusterItem> {
		public boolean onClusterClick(Cluster<T> cluster);
	}

	/**
	 * Called when a Cluster's Info Window is clicked.
	 */
	public interface OnClusterInfoWindowClickListener<T extends ClusterItem> {
		public void onClusterInfoWindowClick(Cluster<T> cluster);
	}

	/**
	 * Called when an individual ClusterItem is clicked.
	 */
	public interface OnClusterItemClickListener<T extends ClusterItem> {
		public boolean onClusterItemClick(T item);
	}

	/**
	 * Called when an individual ClusterItem's Info Window is clicked.
	 */
	public interface OnClusterItemInfoWindowClickListener<T extends ClusterItem> {
		public void onClusterItemInfoWindowClick(T item);
	}
}
